/*
 * ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Mozilla Communicator client code, released
 * March 31, 1998.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1998
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Daniel Veditz <dveditz@netscape.com>
 *   Samir Gehani <sgehani@netscape.com>
 *   Mitch Stoltz <mstoltz@netsape.com>
 *   Pierre Phaneuf <pp@ludusdesign.com>
 *   Jeroen Dobbelaere <jeroen.dobbelaere@acunia.com>
 *   Jeff Walden <jwalden+code@mit.edu>
 *
 *   Lan Qiang <jameslan@gmail.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

#include "string.h"
#include "nsZipEntryIndex.h"
#include "nsILocalFile.h"
#include "nsString.h"
#include "zipformat.h"
#include "nsRecyclingAllocator.h"
#include "nsAutoLock.h"
#include "nsZipInputStream.h"
#include "nsZipOutputStream.h"
#include "nsZipFileIO.h"

#if defined(XP_UNIX) || defined(XP_BEOS)
#include "unistd.h"
#endif

#if defined(XP_UNIX) || defined(XP_BEOS)
static nsresult ResolveSymlink(const char *path);
#endif
static PRTime GetModTime(PRUint16 aDate, PRUint16 aTime);
static PRUint16 GetZipDate(PRTime aTime);
static PRUint16 GetZipTime(PRTime aTime);
extern PRUint32 PR_Tell(PRFileDesc* aFd);
extern nsresult PR_Truncate(PRFileDesc* aFd, PRUint32 newEOF);

/**
 * Globals
 *
 * Global allocator used with zlib. Destroyed in module shutdown.
 */
nsRecyclingAllocator *gZlibAllocator = NULL;

////////////////////////////////////////////////////////////////////////////////
// nsZipIOStatus Constructor/Destructor/Supports

NS_IMPL_THREADSAFE_ISUPPORTS1(nsZipIOStatus, nsIZipIOStatus)

nsZipIOStatus::nsZipIOStatus()
	: mBytesTotal(0),
	  mBytesProcessed(0),
	  mBytesWritten(0),
	  mCompression(0),
	  mAction(idle)
{
}

////////////////////////////////////////////////////////////////////////////////
// nsIZipIOStatus methods

NS_IMETHODIMP nsZipIOStatus::GetZipEntry(char * *aZipEntry)
{
	NS_ENSURE_ARG_POINTER(aZipEntry);

	return NS_ERROR_NOT_IMPLEMENTED;
}

NS_IMETHODIMP nsZipIOStatus::GetIoFile(nsIFile * *anIoFile)
{
	NS_ENSURE_ARG_POINTER(anIoFile);

	*anIoFile = mIOFile;
	NS_IF_ADDREF(*anIoFile);
	return NS_OK;
}

NS_IMETHODIMP nsZipIOStatus::GetBytesTotal(PRUint64 *aBytesTotal)
{
	NS_ENSURE_ARG_POINTER(aBytesTotal);

	*aBytesTotal = mBytesTotal;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOStatus::GetBytesProcessed(PRUint64 *aBytesProcessed)
{
	NS_ENSURE_ARG_POINTER(aBytesProcessed);

	*aBytesProcessed = mBytesProcessed;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOStatus::GetBytesWritten(PRUint64 *aBytesWritten)
{
	NS_ENSURE_ARG_POINTER(aBytesWritten);

	*aBytesWritten = mBytesWritten;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOStatus::GetCompression(PRUint16 *aCompression)
{
	NS_ENSURE_ARG_POINTER(aCompression);

	*aCompression = mCompression;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOStatus::GetAction(nsZipAction *anAction)
{
	NS_ENSURE_ARG_POINTER(anAction);

	*anAction = mAction;
	return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
// nsZipFileIO Constructor/Destructor/Supports

NS_IMPL_THREADSAFE_ISUPPORTS2(nsZipFileIO, nsIZipReader, nsIZipFileIO)

nsZipFileIO::nsZipFileIO()
	: mLock(nsnull),
	  mFd(0),
	  mModified(PR_FALSE),
	  mIndex(nsnull),
	  mCentralDirPos(0)
{
}

nsZipFileIO::~nsZipFileIO()
{
	Close();
}

////////////////////////////////////////////////////////////////////////////////
// nsIZipReader methods:

NS_IMETHODIMP nsZipFileIO::Open(nsIFile *zipFile)
{
	nsresult rv;

	rv = OpenWithFlags(zipFile, PR_RDWR);
	NS_ENSURE_SUCCESS(rv, rv);

	return mIndex->ReadEntryIndex(&mCentralDirPos);
}

NS_IMETHODIMP nsZipFileIO::GetFile(nsIFile * *aFile)
{
	NS_ENSURE_ARG_POINTER(aFile);

	*aFile = mZipFile;
	NS_IF_ADDREF(*aFile);
	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::Close()
{
	if (!mLock) { // not open!
		return NS_OK;
	}

	if (mModified) {
		nsresult rv = mIndex->WriteEntryIndex(mCentralDirPos);
		NS_ENSURE_SUCCESS(rv, rv);
		mModified = PR_FALSE;
	}

	delete mIndex;
	mIndex = nsnull;

	PR_Close(mFd);
	mFd = 0;

	PR_DestroyLock(mLock);
	mLock = nsnull;

	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::Test(const char *aEntryName)
{
	nsZipEntryItem* currItem;
	nsresult rv;

	if (aEntryName) { // only test specified item
		rv = mIndex->GetItem(aEntryName, &currItem);
		if (!currItem)
			return NS_ERROR_FILE_TARGET_DOES_NOT_EXIST;
		//-- don't test synthetic items -- I think for a normal zip
		//-- you actually *can* get away with testing a synthetic
		//-- with no ill results except some unnecessary work, but
		//-- for a zip which doesn't start with a zip entry (e.g.,
		//-- a self-extracting zip) it'll fail
		if (currItem->isSynthetic)
			return NS_OK;
		return ExtractFile(currItem, 0, 0);
	}

	// test all items in archive
	for (nsZipEntryItem *i = mIndex->GetBegin(); i; i = i->next) {
		if (i->isSynthetic || i->isDirectory)
			continue;
		nsresult rv = ExtractFile(i, 0, 0);
		NS_ENSURE_SUCCESS(rv, rv);
	}

	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::Extract(const char *zipEntry, nsIFile *outFile)
{
	// zlib are not thread safe
	// we need to use a lock to prevent bug #51267
	nsAutoLock lock(mLock);

	nsresult rv;
	nsCOMPtr<nsILocalFile> localFile = do_QueryInterface(outFile, &rv);
	if (NS_FAILED(rv)) return rv;

	nsZipEntryItem *item;
	rv = mIndex->GetItem(zipEntry, &item);
	NS_ENSURE_SUCCESS(rv, NS_ERROR_FILE_TARGET_DOES_NOT_EXIST);

	// Remove existing file or directory so we set permissions correctly.
	// If it's a directory that already exists and contains files, throw
	// an exception and return.

	//XXX Bug 332139:
	//XXX If we guarantee that rv in the case of a non-empty directory
	//XXX is always FILE_DIR_NOT_EMPTY, we can remove
	//XXX |rv == NS_ERROR_FAILURE| - bug 322183 needs to be completely
	//XXX fixed before that can happen
	rv = localFile->Remove(PR_FALSE);
	if (rv == NS_ERROR_FILE_DIR_NOT_EMPTY || rv == NS_ERROR_FAILURE)
		return rv;

	if (item->isDirectory) {
		rv = localFile->Create(nsIFile::DIRECTORY_TYPE, item->mode);
		//XXX Do this in nsZipArchive?  It would be nice to keep extraction
		//XXX code completely there, but that would require a way to get a
		//XXX PRDir from localFile.
	} else {
		PRFileDesc* fd;
		rv = localFile->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, item->mode, &fd);
		if (NS_FAILED(rv)) return rv;

		// ExtractFile also closes the fd handle and resolves the symlink if needed
		nsCAutoString path;
		rv = outFile->GetNativePath(path);
		if (NS_FAILED(rv)) return rv;

		rv = ExtractFile(item, path.get(), fd);
	}
	if (NS_FAILED(rv)) return rv;

	PRTime prtime = GetModTime(item->date, item->time);
	// nsIFile needs milliseconds, while prtime is in microseconds.
	PRTime conversion = LL_ZERO;
	PRTime newTime = LL_ZERO;
	LL_I2L(conversion, PR_USEC_PER_MSEC);
	LL_DIV(newTime, prtime, conversion);
	// non-fatal if this fails, ignore errors
	outFile->SetLastModifiedTime(newTime);

	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::GetEntry(const char *zipEntry, nsIZipEntry **_retval)
{
	NS_ENSURE_ARG_POINTER(_retval);

	nsZipEntryItem *item;
	nsresult rv;

	rv = mIndex->GetItem(zipEntry, &item);
	NS_ENSURE_SUCCESS(rv, rv);

	nsZipIOEntry *entry = new nsZipIOEntry(item);
	if (!entry) {
		return NS_ERROR_OUT_OF_MEMORY;
	}
	
	NS_ADDREF(*_retval = entry);
	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::HasEntry(const nsACString & zipEntry, PRBool *_retval)
{
	NS_ENSURE_ARG_POINTER(_retval);

	nsZipEntryItem *item;
	nsresult rv;

	rv = mIndex->GetItem(PromiseFlatCString(zipEntry).get(), &item);
	NS_ENSURE_SUCCESS(rv, rv);

	*_retval = (item != nsnull);

	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::FindEntries(const char *aPattern, nsIUTF8StringEnumerator **_retval)
{
	NS_ENSURE_ARG_POINTER(_retval);

	nsZipEntryFind *find = new nsZipEntryFind;
	if (!find) {
		return NS_ERROR_OUT_OF_MEMORY;
	}

	nsresult rv;
	rv = find->Init(mIndex, aPattern);
	if (NS_FAILED(rv)) {
		delete find;
		return rv;
	}

	nsZipEntryEnumerator *enumerator = new nsZipEntryEnumerator(find);
	if (!enumerator) {
		delete find;
		return NS_ERROR_OUT_OF_MEMORY;
	}

	NS_ADDREF(*_retval = enumerator);
	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::GetInputStream(const char *zipEntry, nsIInputStream **_retval)
{
	NS_ENSURE_ARG_POINTER(_retval);
	NS_ENSURE_ARG_POINTER(zipEntry);

	nsZipEntryItem *item;
	nsresult rv = mIndex->GetItem(zipEntry, &item);
	NS_ENSURE_SUCCESS(rv, rv);
	
	nsZipInputStream* is = new nsZipInputStream;
	if (!is) {
		return NS_ERROR_OUT_OF_MEMORY;
	}
	NS_ADDREF(is);

	rv = is->Init(this, item, mFd);
	if (NS_FAILED(rv)) {
		NS_RELEASE(is);
		return rv;
	}

	*_retval = is;

	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::GetInputStreamWithSpec(const nsACString & aJarSpec, const char *zipEntry, nsIInputStream **_retval)
{
    return NS_ERROR_NOT_IMPLEMENTED;
}

////////////////////////////////////////////////////////////////////////////////
// nsIZipFileIO methods:

NS_IMETHODIMP nsZipFileIO::Create(nsIFile *zipFile)
{
	mModified = PR_TRUE;

	return OpenWithFlags(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
}

NS_IMETHODIMP nsZipFileIO::AddEntry(const char *zipEntry, nsZipLevel compression, nsIOutputStream **_retval)
{
	NS_ENSURE_ARG_POINTER(_retval);
	NS_ENSURE_ARG_POINTER(zipEntry);

	PRUint32 namelen = strlen(zipEntry);
	nsZipEntryItem *item = (nsZipEntryItem*)malloc(sizeof(nsZipEntryItem) + namelen);
	if (!item) {
		return NS_ERROR_OUT_OF_MEMORY;
	}

	item->time = GetZipTime(PR_Now());
	item->date = GetZipDate(PR_Now());
	item->mode = (PRUint16)0644;
	item->isDirectory = PR_FALSE;
	item->hashNext = item->next = item->prev = nsnull;
	item->offset = mCentralDirPos;
	item->dataOffset = item->offset + sizeof(ZipLocalHdr) + namelen;
	item->size = 0;
	item->itemCRC32 = 0;
	item->compression = compression == ZipLevelStore ? STORED : DEFLATED;
	item->hasDataOffset = PR_TRUE;
	item->isSynthetic = PR_FALSE;

#if defined(XP_UNIX) || defined(XP_BEOS)
	item->isSymlink = PR_FALSE;
#endif

	// setting item->name
	strcpy(item->name, zipEntry);

	nsZipOutputStream* os = new nsZipOutputStream;
	if (!os) {
		return NS_ERROR_OUT_OF_MEMORY;
	}
	NS_ADDREF(os);

	nsresult rv = os->Init(this, item, mIndex, mFd, this, compression);
	if (NS_FAILED(rv)) {
		NS_RELEASE(os);
		return rv;
	}

	mModified = PR_TRUE;

	*_retval = os;

	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::AddEntryFromFile(const char *zipEntry, nsIFile *inFile, nsZipLevel compression)
{
	nsAutoLock lock(mLock);

	nsresult rv;
	nsCAutoString path;
	rv = inFile->GetNativePath(path);
	NS_ENSURE_SUCCESS(rv, rv);

	PRBool isDir;
	rv = inFile->IsDirectory(&isDir);
	NS_ENSURE_SUCCESS(rv, rv);

	PRUint32 namelen = strlen(zipEntry);
	if (isDir) {
		if (zipEntry[namelen-1] != '/') {
			++namelen; // prepare to add suffix '/' to the path
		}
	} else if (zipEntry[namelen-1] == '/') {
		isDir = PR_TRUE;
	}

	// create a nsZipEntryItem
	nsZipEntryItem *item = (nsZipEntryItem*)malloc(sizeof(nsZipEntryItem) + namelen);
	if (!item) {
		return NS_ERROR_OUT_OF_MEMORY;
	}

	// setting item->realSize
	PRInt64 temp;
	if (!isDir) {
		rv = inFile->GetFileSize(&temp);
		if (NS_FAILED(rv)) {
			free(item);
			return rv;
		}
		LL_L2UI(item->realSize, temp);
	}

	// setting modified time
	rv = inFile->GetLastModifiedTime(&temp);
	if (NS_FAILED(rv)) {
		free(item);
		return rv;
	}
	item->time = GetZipTime(temp);
	item->date = GetZipDate(temp);

	// setting item->mode
	PRUint32 permissions;
	rv = inFile->GetPermissions(&permissions);
	if (NS_FAILED(rv)) {
		free(item);
		return rv;
	}
	item->mode = (PRUint16)permissions;

	item->isDirectory = isDir;
	item->hashNext = item->next = item->prev = nsnull;
	item->offset = mCentralDirPos;
	item->dataOffset = item->offset + sizeof(ZipLocalHdr) + namelen;
	item->size = 0;
	item->itemCRC32 = 0;
	item->compression = compression == ZipLevelStore ? STORED : DEFLATED;
	item->hasDataOffset = PR_TRUE;
	item->isSynthetic = PR_FALSE;

	// setting item->name
	strcpy(item->name, zipEntry);
	if (isDir) {
		item->name[namelen-1] = '/';
		item->name[namelen] = '\0';
	}

#if defined(XP_UNIX) || defined(XP_BEOS)
	PRBool symbolic;
	rv = inFile->IsSymlink(&symbolic);
	if (NS_FAILED(rv)) {
		free(item);
		return rv;
	}
	item->isSymlink = symbolic;
#endif

	if (isDir) {
		item->realSize = 0;
		item->compression = STORED;
	}

	rv = CompressFile(item, inFile, compression);
	if (NS_FAILED(rv)) {
		free(item);
	}
	return rv;
}

NS_IMETHODIMP nsZipFileIO::RemoveEntries(nsIUTF8StringEnumerator *zipEntries)
{
	NS_ENSURE_ARG_POINTER(zipEntries);

	nsAutoLock lock(mLock);
	nsresult rv;
	PRBool more;
	PRBool removed = PR_FALSE;

	// remove entry index
	for(;;) {
		rv = zipEntries->HasMore(&more);
		NS_ENSURE_SUCCESS(rv, rv);
		if (!more) 
			break;

		// get name of next entry to be removed
		nsCAutoString entry;
		rv = zipEntries->GetNext(entry);
		NS_ENSURE_SUCCESS(rv, rv);

		rv = mIndex->RemoveFromHashTable(entry.get());
		NS_ENSURE_SUCCESS(rv, rv);
		removed = PR_TRUE;
	}

	if (!removed)
		return NS_OK;

	// pack zip archive
	mModified = PR_TRUE; // central directory needs to be refreshed

	nsZipEntryItem *item = mIndex->GetBegin();
	PRUint32 pos = 0;
	for (; item; item = item->next) {
		if (PR_Seek(mFd, item->offset, PR_SEEK_SET) < 0) {
			return NS_ERROR_FILE_CORRUPTED;
		}
		ZipLocalHdr Local;
		if ((PR_Read(mFd, (char*)&Local, sizeof(Local)) != sizeof(Local)) || 
				(le32_to_host(Local.signature) != LOCALSIG))
		{
			// read error or local header not found
			return NS_ERROR_FILE_CORRUPTED;
		}

		PRUint32 blockLen = sizeof(Local)
			+ le16_to_host(Local.filename_len)
			+ le16_to_host(Local.extrafield_len + Local.size);
		if (Local.bitflag & 8)
			blockLen += sizeof(ZipDataDesc);

		if (pos == item->offset) {
			// this entry sit on the right position
			pos += blockLen;
			item->dataOffset = pos + sizeof(Local)
				+ le16_to_host(Local.filename_len)
				+ le16_to_host(Local.extrafield_len);
			item->hasDataOffset = PR_TRUE;
			continue;
		}

		// move entry data to appropriate position
		char buf[ZIP_PACKING_BUFLEN];
		PRUint32 bytesAvailable = blockLen;
		PRUint32 posReading = item->offset;
		item->offset = pos;
		item->dataOffset = pos + sizeof(Local)
			+ le16_to_host(Local.filename_len)
			+ le16_to_host(Local.extrafield_len);
		item->hasDataOffset = PR_TRUE;

		// moving data
		while (bytesAvailable) {
			PRUint32 bytes = bytesAvailable < ZIP_PACKING_BUFLEN
				? bytesAvailable
				: ZIP_PACKING_BUFLEN;
			// reading
			if (PR_Seek(mFd, posReading, PR_SEEK_SET) < 0) {
				return NS_ERROR_FILE_CORRUPTED;
			}
			if ((PRUint32)PR_Read(mFd, buf, bytes) != bytes) {
				return NS_ERROR_FILE_CORRUPTED;
			}
			posReading += bytes;

			// writing
			if (PR_Seek(mFd, pos, PR_SEEK_SET) < 0) {
				return NS_ERROR_FILE_CORRUPTED;
			}
			if ((PRUint32)PR_Write(mFd, buf, bytes) != bytes) {
				return NS_ERROR_FILE_DISK_FULL;
			}
			pos += bytes;

			bytesAvailable -= bytes;
		} // moving data
	} // for next entry

	mCentralDirPos = pos;
	return NS_OK;
}

NS_IMETHODIMP nsZipFileIO::GetStatus(nsIZipIOStatus * *aStatus)
{
	NS_ENSURE_ARG_POINTER(aStatus);

	return NS_ERROR_NOT_IMPLEMENTED;
}


////////////////////////////////////////////////////////////////////////////////
// nsZipFileIO private implementations

nsresult nsZipFileIO::OpenWithFlags(nsIFile *zipFile, long flags)
{
	if (mLock) return NS_ERROR_FAILURE; // Already open!

	mLock = PR_NewLock();
	mZipFile = zipFile;

	nsresult rv;

	nsCOMPtr<nsILocalFile> localFile = do_QueryInterface(mZipFile, &rv);
	if (NS_FAILED(rv))
		return NS_ERROR_FILE_UNRECOGNIZED_PATH;

	rv = localFile->OpenNSPRFileDesc(flags, 0000, &mFd);
	NS_ENSURE_SUCCESS(rv, rv);

	mIndex = new nsZipEntryIndex(mFd);
	if (mIndex == nsnull) {
		return NS_ERROR_OUT_OF_MEMORY;
	}

	return NS_OK;
}

nsresult nsZipFileIO::SeekToItem(nsZipEntryItem* anItem)
{
	NS_ENSURE_ARG_POINTER(anItem);

	// the first time an item is used we need to calculate its offset
	if (!anItem->hasDataOffset) {
		// read local header to get variable length values and calculate
		// the real data offset
		//
		// NOTE: extralen is different in central header and local header
		//       for archives created using the Unix "zip" utility. To set
		//       the offset accurately we need the _local_ extralen.
		if (PR_Seek(mFd, anItem->offset, PR_SEEK_SET) < 0)
			return NS_ERROR_FILE_CORRUPTED;

		ZipLocalHdr Local;
		if ((PR_Read(mFd, (char*)&Local, sizeof(Local)) != sizeof(Local)) || 
				(le32_to_host(Local.signature) != LOCALSIG))
		{
			// read error or local header not found
			return NS_ERROR_FILE_CORRUPTED;
		}

		anItem->dataOffset = anItem->offset +
			sizeof(Local)+
			le16_to_host(Local.filename_len) +
			le16_to_host(Local.extrafield_len);
		anItem->hasDataOffset = PR_TRUE;
	}

	// move to start of entry in archive
	if (!PR_Seek(mFd, anItem->dataOffset, PR_SEEK_SET))
		return  NS_ERROR_FILE_CORRUPTED;

	return NS_OK;
}

/**
 * This function inflates an archive item to disk, to the
 * file specified by outFD. If outFD is zero, the extracted data is
 * not written, only checked for CRC, so this is in effect same as 'Test'.
 */
nsresult nsZipFileIO::InflateItem(const nsZipEntryItem* aItem, PRFileDesc* outFD)
{
	NS_ENSURE_ARG_POINTER(aItem);

	//-- allocate deflation buffers
	Bytef inbuf[ZIP_BUFLEN];
	Bytef outbuf[ZIP_BUFLEN];

	//-- set up the inflate
	z_stream zs;
	nsresult status = gZlibInit(&zs);
	if (status != NS_OK)
		return NS_ERROR_FAILURE;
	int zerr = inflateInit2(&zs, -MAX_WBITS);
	if (zerr != Z_OK) return NS_ERROR_OUT_OF_MEMORY;

	//-- inflate loop
	zs.next_out = outbuf;
	zs.avail_out = ZIP_BUFLEN;

	PRUint32  size = aItem->size;
	PRUint32  outpos = 0;
	PRUint32  crc = crc32(0L, Z_NULL, 0);
	zerr = Z_OK;
	while (zerr == Z_OK) {
		PRBool      bRead = PR_FALSE;
		PRBool      bWrote= PR_FALSE;

		if (zs.avail_in == 0 && zs.total_in < size) {
			//-- no data to inflate yet still more in file:
			//-- read another chunk of compressed data
			PRUint32 chunk = (size-zs.total_in < ZIP_BUFLEN) ? size-zs.total_in : ZIP_BUFLEN;

			if ((PRUint32)PR_Read(mFd, inbuf, chunk) != chunk) {
				//-- unexpected end of data
				status = NS_ERROR_FILE_CORRUPTED;
				break;
			}

			zs.next_in  = inbuf;
			zs.avail_in = chunk;
			bRead       = PR_TRUE;
		}

		if (zs.avail_out == 0) {
			//-- write inflated buffer to disk and make space
			if (outFD && PR_Write(outFD, outbuf, ZIP_BUFLEN) < ZIP_BUFLEN) {
				//-- Couldn't write all the data (disk full?)
				status = NS_ERROR_FILE_DISK_FULL;
				break;
			}

			outpos = zs.total_out;
			zs.next_out  = outbuf;
			zs.avail_out = ZIP_BUFLEN;
			bWrote       = PR_TRUE;
		}

		if(bRead || bWrote) {
			Bytef* old_next_out = zs.next_out;

			zerr = inflate(&zs, Z_NO_FLUSH);

			//-- incrementally update crc32
			crc = crc32(crc, (const unsigned char*)old_next_out, zs.next_out - old_next_out);
		} else
			zerr = Z_STREAM_END;

	} // while

	//-- verify crc32
	if ((status == NS_OK) && (crc != aItem->itemCRC32)) {
		status = NS_ERROR_FILE_CORRUPTED;
		goto inflate_cleanup;
	}

	//-- write last inflated bit to disk
	if (zerr == Z_STREAM_END && outpos < zs.total_out) {
		PRUint32 chunk = zs.total_out - outpos;
		if (outFD && (PRUint32)PR_Write(outFD, outbuf, chunk) < chunk)
			status = NS_ERROR_FILE_DISK_FULL;
	}

	//-- convert zlib error to return value
	if (status == NS_OK && zerr != Z_OK && zerr != Z_STREAM_END) {
		status = (zerr == Z_MEM_ERROR) ? NS_ERROR_OUT_OF_MEMORY: NS_ERROR_FILE_CORRUPTED;
	}

	//-- if found no errors make sure we've converted the whole thing
	PR_ASSERT(status != NS_OK || zs.total_in == aItem->size);
	PR_ASSERT(status != NS_OK || zs.total_out == aItem->realSize);

inflate_cleanup:
	//-- free zlib internal state
	inflateEnd(&zs);

	return status;
}

/**
 * nsZipFileIO::ExtractFile
 * This extracts the item to the filehandle provided.
 * If 'aFd' is null, it only tests the extraction.
 * On extraction error(s) it removes the file.
 * When needed, it also resolves the symlink.
 */
nsresult nsZipFileIO::ExtractFile(nsZipEntryItem *item, const char *outname,
                                   PRFileDesc* aFd)
{
	NS_ENSURE_ARG_POINTER(item);
	if (!mFd)
		return NS_ERROR_FAILURE;

	// Directory extraction is handled in nsJAR::Extract,
	// so the item to be extracted should never be a directory
	PR_ASSERT(!item->isDirectory);

	//-- move to the start of file's data
	if (SeekToItem(item) != NS_OK)
		return NS_ERROR_FILE_CORRUPTED;

	nsresult rv;

	//-- extract the file using the appropriate method
	switch(item->compression) {
	case STORED:
		rv = CopyItemToDisk(item->size, item->itemCRC32, aFd);
		break;

	case DEFLATED:
		rv = InflateItem(item, aFd);
		break;

	default:
		//-- unsupported compression type
		rv = NS_ERROR_NOT_IMPLEMENTED;
	}

	//-- delete the file on errors, or resolve symlink if needed
	if (aFd) {
		PR_Close(aFd);
		if (rv != NS_OK)
			PR_Delete(outname);
#if defined(XP_UNIX) || defined(XP_BEOS)
		else if (item->isSymlink)
			rv = ResolveSymlink(outname);
#endif
	}

	return rv;
}

/**
 * This function copies an archive item to disk, to the
 * file specified by outFD. If outFD is zero, the extracted data is
 * not written, only checked for CRC, so this is in effect same as 'Test'.
 */
nsresult nsZipFileIO::CopyItemToDisk(PRUint32 itemSize, PRUint32 itemCrc,
		PRFileDesc* outFD)
{
	PRUint32    chunk, pos, crc;
	char buf[ZIP_BUFLEN];

	//-- initialize crc
	crc = crc32(0L, Z_NULL, 0);

	//-- copy chunks until file is done
	for (pos = 0; pos < itemSize; pos += chunk) {
		chunk = (itemSize - pos < ZIP_BUFLEN)
			? (itemSize - pos)
			: ZIP_BUFLEN;

		if ((PRUint32)PR_Read(mFd, buf, chunk) != chunk) {
			//-- unexpected end of data in archive
			return NS_ERROR_FILE_CORRUPTED;
		}

		//-- incrementally update crc32
		crc = crc32(crc, (const unsigned char*)buf, chunk);

		if (outFD && (PRUint32)PR_Write(outFD, buf, chunk) < chunk) {
			//-- Couldn't write all the data (disk full?)
			return NS_ERROR_FILE_DISK_FULL;
		}
	}

	//-- verify crc32
	if (crc != itemCrc)
		return NS_ERROR_FILE_CORRUPTED;

	return NS_OK;
}

nsresult nsZipFileIO::CompressFile(nsZipEntryItem *item, nsIFile* inFile, nsZipLevel compression)
{
	nsresult rv;
	nsCOMPtr<nsILocalFile> localFile = do_QueryInterface(inFile, &rv);
	NS_ENSURE_SUCCESS(rv, rv);

	mModified = PR_TRUE;

	// seek to local file header
	if (PR_Seek(mFd, item->offset, PR_SEEK_SET) < 0) {
		return NS_ERROR_FILE_CORRUPTED;
	}

	// write local file header
	rv = mIndex->WriteLocalHeader(item);
	NS_ENSURE_SUCCESS(rv, rv);

	// compressed file
	PRFileDesc* fd;
	rv = localFile->OpenNSPRFileDesc(PR_RDONLY, 0000, &fd);
	NS_ENSURE_SUCCESS(rv, rv);

	switch(item->compression) {
	case STORED:
		rv = CopyItemFromDisk(item, fd);
		PR_Close(fd);
		NS_ENSURE_SUCCESS(rv, rv);
		break;
	case DEFLATED:
		rv = DeflateItem(item, fd, compression);
		PR_Close(fd);
		NS_ENSURE_SUCCESS(rv, rv);
		break;
	default: 
		return NS_ERROR_NOT_IMPLEMENTED;
	}

	mCentralDirPos = PR_Tell(mFd);

	// rewrite local file header
	if (PR_Seek(mFd, item->offset, PR_SEEK_SET) < 0) {
		return NS_ERROR_FILE_CORRUPTED;
	}
	rv = mIndex->WriteLocalHeader(item);
	NS_ENSURE_SUCCESS(rv, rv);

	return mIndex->InsertIntoHashTable(item);
}

nsresult nsZipFileIO::DeflateItem(nsZipEntryItem* aItem, PRFileDesc* inFD, nsZipLevel compression)
{
	NS_ENSURE_ARG_POINTER(aItem);

	//-- allocate deflation buffers
	Bytef inbuf[ZIP_BUFLEN];
	Bytef outbuf[ZIP_BUFLEN];

	//-- set up the deflate
	z_stream    zs;
	nsresult status = gZlibInit(&zs);
	if (status != NS_OK)
		return NS_ERROR_FAILURE;
	int zerr = deflateInit2(&zs, compression, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY);
	if (zerr != Z_OK) return NS_ERROR_OUT_OF_MEMORY;

	//-- deflate loop
	zs.next_out = outbuf;
	zs.avail_out = ZIP_BUFLEN;

	PRUint32  size = aItem->realSize;
	PRUint32  crc = crc32(0L, Z_NULL, 0);
	zerr = Z_OK;
	while (zerr == Z_OK) {
		PRBool      bRead = PR_FALSE;
		PRBool      bWrote= PR_FALSE;

		if (zs.avail_in == 0 && zs.total_in < size) {
			// no data to deflate yet still more in file:
			// read another chunk of compressed data
			PRUint32 chunk = (size-zs.total_in < ZIP_BUFLEN) ? size-zs.total_in : ZIP_BUFLEN;

			if ((PRUint32)PR_Read(inFD, inbuf, chunk) != chunk) {
				//-- unexpected end of data
				status = NS_ERROR_FILE_CORRUPTED;
				break;
			}

			// incrementally update crc32
			crc = crc32(crc, (const unsigned char*)inbuf, chunk);

			zs.next_in  = inbuf;
			zs.avail_in = chunk;
			bRead       = PR_TRUE;
		}

		if (zs.avail_out == 0) {
			// write inflated buffer to disk and make space
			if (PR_Write(mFd, outbuf, ZIP_BUFLEN) < ZIP_BUFLEN) {
				// Couldn't write all the data (disk full?)
				status = NS_ERROR_FILE_DISK_FULL;
				break;
			}

			zs.next_out  = outbuf;
			zs.avail_out = ZIP_BUFLEN;
			bWrote       = PR_TRUE;
		}

		if(bRead || bWrote) {
			zerr = deflate(&zs, Z_NO_FLUSH);
		} else
			zerr = Z_STREAM_END;

	} // while

	//-- write last deflated bit to disk
	if (zerr == Z_STREAM_END) do {
		zerr = deflate(&zs, Z_FINISH);
		PRUint32 chunk = ZIP_BUFLEN - zs.avail_out;
		if ((PRUint32)PR_Write(mFd, outbuf, chunk) < chunk)
			status = NS_ERROR_FILE_DISK_FULL;
		zs.next_out = outbuf;
		zs.avail_out = ZIP_BUFLEN;
	} while (zerr == Z_OK);

	//-- convert zlib error to return value
	if (status == NS_OK && zerr != Z_OK && zerr != Z_STREAM_END) {
		status = (zerr == Z_MEM_ERROR) ? NS_ERROR_OUT_OF_MEMORY: NS_ERROR_FILE_CORRUPTED;
	}

	//-- if found no errors make sure we've converted the whole thing
	PR_ASSERT(status != NS_OK || zs.total_in == aItem->realSize);

	aItem->size = zs.total_out;
	aItem->itemCRC32 = crc;

	//-- free zlib internal state
	deflateEnd(&zs);

	return status;
}

nsresult nsZipFileIO::CopyItemFromDisk(nsZipEntryItem *anItem, PRFileDesc* inFD)
{
	PRUint32    chunk, pos, crc;
	char buf[ZIP_BUFLEN];

	//-- initialize crc
	crc = crc32(0L, Z_NULL, 0);

	//-- copy chunks until file is done
	for (pos = 0; pos < anItem->realSize; pos += chunk) {
		chunk = (anItem->realSize - pos < ZIP_BUFLEN)
			? (anItem->realSize - pos)
			: ZIP_BUFLEN;

		if (inFD && (PRUint32)PR_Read(inFD, buf, chunk) < chunk) {
			//-- unexpected end of data in archive
			return NS_ERROR_FILE_CORRUPTED;
		}

		//-- incrementally update crc32
		crc = crc32(crc, (const unsigned char*)buf, chunk);

		if ((PRUint32)PR_Write(mFd, buf, chunk) != chunk) {
			//-- Couldn't write all the data (disk full?)
			return NS_ERROR_FILE_DISK_FULL;
		}
	}

	anItem->size = pos;
	anItem->itemCRC32 = crc;

	return NS_OK;
}
////////////////////////////////////////////////////////////////////////////////
// nsZipIOEntry implementation

NS_IMPL_ISUPPORTS1(nsZipIOEntry, nsIZipEntry)

nsZipIOEntry::nsZipIOEntry(nsZipEntryItem* anItem)
	: mCompression(anItem->compression),
	  mSize(anItem->size),
	  mRealSize(anItem->realSize),
	  mCRC32(anItem->itemCRC32),
	  mIsDirectory(anItem->isDirectory),
	  mIsSynthetic(anItem->isSynthetic),
	  mTime(GetModTime(anItem->date, anItem->time))
{
}

nsZipIOEntry::~nsZipIOEntry()
{
}

NS_IMETHODIMP nsZipIOEntry::GetCompression(PRUint16 *aCompression)
{
	NS_ENSURE_ARG_POINTER(aCompression);

	*aCompression = mCompression;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOEntry::GetSize(PRUint32 *aSize)
{
	NS_ENSURE_ARG_POINTER(aSize);

	*aSize = mSize;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOEntry::GetRealSize(PRUint32 *aRealSize)
{
	NS_ENSURE_ARG_POINTER(aRealSize);

	*aRealSize = mRealSize;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOEntry::GetCRC32(PRUint32 *aCRC32)
{
	NS_ENSURE_ARG_POINTER(aCRC32);

	*aCRC32 = mCRC32;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOEntry::GetIsDirectory(PRBool *aIsDirectory)
{
	NS_ENSURE_ARG_POINTER(aIsDirectory);

	*aIsDirectory = mIsDirectory;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOEntry::GetLastModifiedTime(PRTime *aLastModifiedTime)
{
	NS_ENSURE_ARG_POINTER(aLastModifiedTime);

	*aLastModifiedTime = mTime;
	return NS_OK;
}

NS_IMETHODIMP nsZipIOEntry::GetIsSynthetic(PRBool *aIsSynthetic)
{
	NS_ENSURE_ARG_POINTER(aIsSynthetic);

	*aIsSynthetic = mIsSynthetic;
	return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
// nsZipEntryEnumerator implementation

NS_IMPL_THREADSAFE_ISUPPORTS1(nsZipEntryEnumerator, nsIUTF8StringEnumerator)

nsZipEntryEnumerator::~nsZipEntryEnumerator()
{
	delete mFind;
}

NS_IMETHODIMP nsZipEntryEnumerator::HasMore(PRBool *_retval)
{
	// try to get the next element
	if (!mCurr) {
		NS_ASSERTION(mFind, "nsZipEnumerator: Missing zipFind.");
		nsresult rv = mFind->FindNext(&mCurr);
		if (rv == NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) {
			*_retval = PR_FALSE; // No more matches available
			return NS_OK;
		}
		NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); // no error translation
	}

	*_retval = PR_TRUE;
	return NS_OK;
}

NS_IMETHODIMP nsZipEntryEnumerator::GetNext(nsACString & _retval)
{
	// check if the current item is "stale"
	if (!mCurr) {
		PRBool   bMore;
		nsresult rv = HasMore(&bMore);
		if (NS_FAILED(rv) || !bMore)
			return NS_ERROR_FAILURE; // no error translation
	}
	_retval = mCurr;
	mCurr = 0; // we just gave this one away
	return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
// helper functions

/**
 * Allocators for use with zlib
 *
 * These are allocators that are performance tuned for
 * use with zlib. Our use of zlib for every file we read from
 * the jar file when running navigator, we do these allocation.
 * alloc 24
 * alloc 64
 * alloc 11520
 * alloc 32768
 * alloc 1216 [304x4] max
 * alloc 76   [19x4]
 * free  76   [19x4]
 * alloc 1152 [288x4]
 * free  1152 [288x4]
 * free  1216 [304x4]
 * alloc 28
 * free  28
 * free  32768
 * free  11520
 * free  64
 * free  24
 *
 * The pool will allocate these as:
 *
 *          32,768
 *          11,520
 *           1,280 [320x4] - shared by first x4 alloc, 28
 *           1,280 [320x4] - shared by second and third x4 alloc
 *              64
 *              24
 *          ------
 *          46,936
 *
 * And almost all of the file reads happen serially. Hence this
 * allocator tries to keep one set of memory needed for one file around
 * and reused the same blocks for other file reads.
 *
 * The interesting question is when should be free this ?
 * - memory pressure should be one.
 * - after startup of navigator
 * - after startup of mail
 * In general, this allocator should be enabled before
 * we startup and disabled after we startup if memory is a concern.
 */

void * PR_CALLBACK zlibAlloc(void *opaque, uInt items, uInt size)
{
	nsRecyclingAllocator *zallocator = (nsRecyclingAllocator *)opaque;
	if (zallocator) {
		// Bump up x4 allocations
		PRUint32 realitems = items;
		if (size == 4 && items < BY4ALLOC_ITEMS)
			realitems = BY4ALLOC_ITEMS;
		return zallocator->Calloc(realitems, size);
	}
	else
		return calloc(items, size);
}

void PR_CALLBACK zlibFree(void *opaque, void *ptr)
{
	nsRecyclingAllocator *zallocator = (nsRecyclingAllocator *)opaque;
	if (zallocator)
		zallocator->Free(ptr);
	else
		free(ptr);
	return;
}

nsresult gZlibInit(z_stream *zs)
{
	memset(zs, 0, sizeof(z_stream));
	//-- ensure we have our zlib allocator for better performance
	if (!gZlibAllocator) {
		gZlibAllocator = new nsRecyclingAllocator(NBUCKETS, NS_DEFAULT_RECYCLE_TIMEOUT, "libzip");
	}
	if (gZlibAllocator) {
		zs->zalloc = zlibAlloc;
		zs->zfree = zlibFree;
		zs->opaque = gZlibAllocator;
	}

	return NS_OK;
}

static PRTime GetModTime(PRUint16 aDate, PRUint16 aTime)
{
	char buffer[17];

	PR_snprintf(buffer, sizeof(buffer), "%02d/%02d/%04d %02d:%02d",
			((aDate >> 5) & 0x0F), (aDate & 0x1F), (aDate >> 9) + 1980,
			((aTime >> 11) & 0x1F), ((aTime >> 5) & 0x3F)
		   );

	PRTime result;
	PR_ParseTimeString(buffer, PR_FALSE, &result);
	return result;
}

static PRUint16 GetZipDate(PRTime aTime)
{
	PRExplodedTime etime;
	PR_ExplodeTime(aTime, PR_GMTParameters, &etime);
	return (etime.tm_mday) | (etime.tm_month << 5) | ((etime.tm_year-1980) << 9);
}

static PRUint16 GetZipTime(PRTime aTime)
{
	PRExplodedTime etime;
	PR_ExplodeTime(aTime, PR_GMTParameters, &etime);
	return (etime.tm_min << 5) | (etime.tm_hour << 11);
}

#if defined(XP_UNIX) || defined(XP_BEOS)

//---------------------------------------------
// ResolveSymlink
//---------------------------------------------
static nsresult ResolveSymlink(const char *path)
{
	PRFileDesc * fIn = PR_Open(path, PR_RDONLY, 0000);
	if (!fIn)
		return NS_ERROR_FILE_DISK_FULL;

	char buf[PATH_MAX+1];
	PRInt32 length = PR_Read(fIn, (void*)buf, PATH_MAX);
	PR_Close(fIn);

	if ( (length <= 0)
			|| ((buf[length] = 0, PR_Delete(path)) != 0)
			|| (symlink(buf, path) != 0))
	{
		return NS_ERROR_FILE_DISK_FULL;
	}
	return NS_OK;
}

#endif
